// 版权所有2009年围棋作者。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// Godoc注释提取和注释->HTML格式。

package doc

import (
	"bytes"
	"internal/lazyregexp"
	"io"
	"strings"
	"text/template" // 用于HTMLEscape
	"unicode"
	"unicode/utf8"
)

const (
	ldquo = "&ldquo;"
	rdquo = "&rdquo;"
	ulquo = "“"
	urquo = "”"
)

var (
	htmlQuoteReplacer    = strings.NewReplacer(ulquo, ldquo, urquo, rdquo)
	unicodeQuoteReplacer = strings.NewReplacer("``", ulquo, "''", urquo)
)

// 转义HTML注释文本。如果设置了nice，
// 也要把“变成”；和“进入”；。
func commentEscape(w io.Writer, text string, nice bool) {
	if nice {
		// 在第一步中，我们将``和''转换为它们的unicode等价物。
		// 这可以防止它们在HTMLEscape中被转义。
		text = convertQuotes(text)
		var buf bytes.Buffer
		template.HTMLEscape(&buf, []byte(text))
		// 现在，我们将unicode引号转换为它们的HTML转义实体，以维护旧的行为。
		// 我们需要使用临时缓冲区读回字符串并进行转换，
		// 否则HTMLEscape将逃逸到&amp；
		htmlQuoteReplacer.WriteString(w, buf.String())
		return
	}
	template.HTMLEscape(w, []byte(text))
}

func convertQuotes(text string) string {
	return unicodeQuoteReplacer.Replace(text)
}

const (
	// 用于Go标识符的Regexp
	identRx = `[\pL_][\pL_0-9]*`

	// 用于URL的Regexp
	// 匹配帕伦斯，稍后检查平衡-参见#5043，#22285
	// 匹配。，：；？！在路径内，但不在末端-参见#18139，#16565
	// 这就排除了一些以常用标点符号结尾的罕见但有效的URL
	// 以允许以URL结尾的句子。

	// 协议（必需），例如http
	protoPart = `(https?|ftp|file|gopher|mailto|nntp)`
	// 主机（必需），例如www.example.com或[：：1]：8080
	hostPart = `([a-zA-Z0-9_@\-.\[\]:]+)`
	// 路径+查询+片段（可选），例如/path/index.html？q=foo#bar
	pathPart = `([.,:;?!]*[a-zA-Z0-9$'()*+&#=@~_/\-\[\]%])*`

	urlRx = protoPart + `:// `+主机部分+路径部分
)

var matchRx = lazyregexp.New(`(` + urlRx + `)|(` + identRx + `)`)

var (
	html_a      = []byte(`<a href="`)
	html_aq     = []byte(`">`)
	html_enda   = []byte("</a>")
	html_i      = []byte("<i>")
	html_endi   = []byte("</i>")
	html_p      = []byte("<p>\n")
	html_endp   = []byte("</p>\n")
	html_pre    = []byte("<pre>")
	html_endpre = []byte("</pre>\n")
	html_h      = []byte(`<h3 id="`)
	html_hq     = []byte(`">`)
	html_endh   = []byte("</h3>\n")
)

// 为HTML强调并转义一行文本。URL被转换成链接；
// 如果URL也出现在单词地图中，则链接取自地图（如果
// 对应的映射值为空字符串，不转换URL
// 进入链接）。单词映射中出现的Go标识符为斜体；如果
// 对应的映射值不是空字符串，它被视为URL
// 这个词被转换成一个链接。如果设置了nice，则剩余文本的
// 外观在有意义的地方得到改善（例如，``变为&ldquo；
// 和“进入&rdquo；）。
func emphasize(w io.Writer, line string, words map[string]string, nice bool) {
	for {
		m := matchRx.FindStringSubmatchIndex(line)
		if m == nil {
			break
		}
		// m>=6（matchRx中有两个括号内的子regexp，第一个是urlRx）

		// 在匹配之前写入文本
		commentEscape(w, line[0:m[0]], nice)

		// 调整URL的匹配
		match := line[m[0]:m[1]]
		if strings.Contains(match, ":// ") {
			m0, m1 := m[0], m[1]
			for _, s := range []string{"()", "{}", "[]"} {
				open, close := s[:1], s[1:] // 例如，“和”
				// 在结束括号之前需要开始括号（#22285）
				if i := strings.Index(match, close); i >= 0 && i < strings.Index(match, open) {
					m1 = m0 + i
					match = line[m0:m1]
				}
				// 需要平衡的括号对（#5043）
				for i := 0; strings.Count(match, open) != strings.Count(match, close) && i < 10; i++ {
					m1 = strings.LastIndexAny(line[:m1], s)
					match = line[m0:m1]
				}
			}
			if m1 != m[1] {
				// 使用缩短的行重做匹配以获得正确的索引
				m = matchRx.FindStringSubmatchIndex(line[:m[0]+len(match)])
			}
		}

		// 分析匹配
		url := ""
		italics := false
		if words != nil {
			url, italics = words[match]
		}
		if m[2] >= 0 {
			// 与第一个括号内的子regexp匹配；必须与urlRx匹配
			if !italics {
				// 单词列表中没有其他URL，请使用匹配
				url = match
			}
			italics = false // 不要将URL斜体化
		}

		// 写匹配
		if len(url) > 0 {
			w.Write(html_a)
			template.HTMLEscape(w, []byte(url))
			w.Write(html_aq)
		}
		if italics {
			w.Write(html_i)
		}
		commentEscape(w, match, nice)
		if italics {
			w.Write(html_endi)
		}
		if len(url) > 0 {
			w.Write(html_enda)
		}

		// 进展
		line = line[m[1]:]
	}
	commentEscape(w, line, nice)
}

func indentLen(s string) int {
	i := 0
	for i < len(s) && (s[i] == ' ' || s[i] == '\t') {
		i++
	}
	return i
}

func isBlank(s string) bool {
	return len(s) == 0 || (len(s) == 1 && s[0] == '\n')
}

func commonPrefix(a, b string) string {
	i := 0
	for i < len(a) && i < len(b) && a[i] == b[i] {
		i++
	}
	return a[0:i]
}

func unindent(block []string) {
	if len(block) == 0 {
		return
	}

	// 计算最大公共白色前缀
	prefix := block[0][0:indentLen(block[0])]
	for _, line := range block {
		if !isBlank(line) {
			prefix = commonPrefix(prefix, line[0:indentLen(line)])
		}
	}
	n := len(prefix)

	// 去除
	for i, line := range block {
		if !isBlank(line) {
			block[i] = line[n:]
		}
	}
}

// heading返回作为节标题传递的修剪线；
// 否则返回空字符串。
func heading(line string) string {
	line = strings.TrimSpace(line)
	if len(line) == 0 {
		return ""
	}

	// 标题必须以大写字母开头
	r, _ := utf8.DecodeRuneInString(line)
	if !unicode.IsLetter(r) || !unicode.IsUpper(r) {
		return ""
	}

	// 它必须以字母或数字结尾：
	r, _ = utf8.DecodeLastRuneInString(line)
	if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
		return ""
	}

	// 排除包含非法字符的行。我们允许“（）”
	if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") {
		return ""
	}

	// 只允许将“'”用于所有格“'s”
	for b := line; ; {
		i := strings.IndexRune(b, '\'')
		if i < 0 {
			break
		}
		if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') {
			return "" // 不后跟“s”
		}
		b = b[i+2:]
	}

	// 后跟非空格时允许“.”
	for b := line; ; {
		i := strings.IndexRune(b, '.')
		if i < 0 {
			break
		}
		if i+1 >= len(b) || b[i+1] == ' ' {
			return "" // 不后跟非空格
		}
		b = b[i+1:]
	}

	return line
}

type op int

const (
	opPara op = iota
	opHead
	opPre
)

type block struct {
	op    op
	lines []string
}

var nonAlphaNumRx = lazyregexp.New(`[^a-zA-Z0-9]`)

func anchorID(line string) string {
	// 添加“hdr-”前缀，以避免与用于包符号的ID冲突。
	return "hdr-" + nonAlphaNumRx.ReplaceAllString(line, "_")
}

// ToHTML将注释文本转换为格式化的HTML。
// 评论由DocReader编写，
// 因此，已知没有前导、尾随空行
// 也不允许在行尾有尾随空格。
// 注释标记已被删除。
// None
// 未缩进的非空行的每个跨度都转换为
// 一段。这条规则有一个例外：一个跨度
// 由一行组成，后面是另一个段落跨度，
// 以大写字母开头，不包含标点符号
// 除括号和逗号外，其他字符的格式为标题。
// None
// 缩进线的跨度转换为<pre>块，
// 删除公共缩进前缀。
// None
// 注释文本中的URL被转换为链接；如果URL也出现
// 在单词map中，链接取自map（如果对应的map
// 值是空字符串，URL不会转换为链接）。
// None
// 一对（连续）反勾号（`）转换为unicode左引号（“），一对（连续）
// 单引号（'）转换为unicode右引号（“）。
// None
// 单词映射中出现的Go标识符为斜体；如果相应的
// 映射值不是空字符串，它被视为URL，并转换为单词
// 变成一个链接。
func ToHTML(w io.Writer, text string, words map[string]string) {
	for _, b := range blocks(text) {
		switch b.op {
		case opPara:
			w.Write(html_p)
			for _, line := range b.lines {
				emphasize(w, line, words, true)
			}
			w.Write(html_endp)
		case opHead:
			w.Write(html_h)
			id := ""
			for _, line := range b.lines {
				if id == "" {
					id = anchorID(line)
					w.Write([]byte(id))
					w.Write(html_hq)
				}
				commentEscape(w, line, true)
			}
			if id == "" {
				w.Write(html_hq)
			}
			w.Write(html_endh)
		case opPre:
			w.Write(html_pre)
			for _, line := range b.lines {
				emphasize(w, line, nil, false)
			}
			w.Write(html_endpre)
		}
	}
}

func blocks(text string) []block {
	var (
		out  []block
		para []string

		lastWasBlank   = false
		lastWasHeading = false
	)

	close := func() {
		if para != nil {
			out = append(out, block{opPara, para})
			para = nil
		}
	}

	lines := strings.SplitAfter(text, "\n")
	unindent(lines)
	for i := 0; i < len(lines); {
		line := lines[i]
		if isBlank(line) {
			// 结束段
			close()
			i++
			lastWasBlank = true
			continue
		}
		if indentLen(line) > 0 {
			// 结束段
			close()

			// 计算缩进行或空行数
			j := i + 1
			for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) {
				j++
			}
			// 但不包括尾随的空行
			for j > i && isBlank(lines[j-1]) {
				j--
			}
			pre := lines[i:j]
			i = j

			unindent(pre)

			// 把这些线放在一个预块中
			out = append(out, block{opPre, pre})
			lastWasHeading = false
			continue
		}

		if lastWasBlank && !lastWasHeading && i+2 < len(lines) &&
			isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 {
			// 当前行为非空，由空行包围
			// 下一个非空行没有缩进：这是
			// 可能是个标题。
			if head := heading(line); head != "" {
				close()
				out = append(out, block{opHead, []string{head}})
				i += 2
				lastWasHeading = true
				continue
			}
		}

		// 开放段落
		lastWasBlank = false
		lastWasHeading = false
		para = append(para, lines[i])
		i++
	}
	close()

	return out
}

// ToText为文本输出中的演示准备注释文本。
// 它将文本段落包装为宽度或更少的Unicode代码点
// 然后在每行前面加上缩进。在预先格式化的部分中
// （如程序文本），它在每个非空行前面加上预缩进。
// None
// 一对（连续）反勾号（`）转换为unicode左引号（“），一对（连续）
// 单引号（'）转换为unicode右引号（“）。
func ToText(w io.Writer, text string, indent, preIndent string, width int) {
	l := lineWrapper{
		out:    w,
		width:  width,
		indent: indent,
	}
	for _, b := range blocks(text) {
		switch b.op {
		case opPara:
			// l、 如果需要，write将添加前导换行符
			for _, line := range b.lines {
				line = convertQuotes(line)
				l.write(line)
			}
			l.flush()
		case opHead:
			w.Write(nl)
			for _, line := range b.lines {
				line = convertQuotes(line)
				l.write(line + "\n")
			}
			l.flush()
		case opPre:
			w.Write(nl)
			for _, line := range b.lines {
				if isBlank(line) {
					w.Write([]byte("\n"))
				} else {
					w.Write([]byte(preIndent))
					w.Write([]byte(line))
				}
			}
		}
	}
}

type lineWrapper struct {
	out       io.Writer
	printed   bool
	width     int
	indent    string
	n         int
	pendSpace int
}

var nl = []byte("\n")
var space = []byte(" ")
var prefix = []byte("// ")

func (l *lineWrapper) write(text string) {
	if l.n == 0 && l.printed {
		l.out.Write(nl) // 新段落前的空行
	}
	l.printed = true

	needsPrefix := false
	isComment := strings.HasPrefix(text, "// ")
	for _, f := range strings.Fields(text) {
		w := utf8.RuneCountInString(f)
		// 如果线太长，请包裹
		if l.n > 0 && l.n+l.pendSpace+w > l.width {
			l.out.Write(nl)
			l.n = 0
			l.pendSpace = 0
			needsPrefix = isComment && !strings.HasPrefix(f, "// ")
		}
		if l.n == 0 {
			l.out.Write([]byte(l.indent))
		}
		if needsPrefix {
			l.out.Write(prefix)
			needsPrefix = false
		}
		l.out.Write(space[:l.pendSpace])
		l.out.Write([]byte(f))
		l.n += l.pendSpace + w
		l.pendSpace = 1
	}
}

func (l *lineWrapper) flush() {
	if l.n == 0 {
		return
	}
	l.out.Write(nl)
	l.pendSpace = 0
	l.n = 0
}
